﻿namespace Moly.Com
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Runtime.InteropServices.ComTypes;
    using Microsoft.Win32;
    using misc;
    using TYPELIBATTR = System.Runtime.InteropServices.ComTypes.TYPELIBATTR;

    [Serializable]
    public class ModuleID
    {
        #region information
        public DepType Type { get; private set; }
        public string Clsid { get; private set; }
        public string Lcid { get; private set; }
        public string VbpPath { get; private set; }
        public string VbpFullPath { get; private set; }
        public bool Upgraded { get; set; }
        public string Desc { get; private set; }
        public BuildVersion Version { get; private set; }
        public ModuleFile File;
      
        #endregion
        #region Constractor

        public static ModuleID FromVbpReferenceLine(string refLine)
        {
            var result = new ModuleID {Type = getTypeFromLine(refLine)};

            refLine = refLine.Substring(refLine.IndexOf("=") + 1);
            var temp = refLine.Split('#');

            switch (result.Type)
            {
                case (DepType.VBP):
                    result.Clsid = temp[0].Substring(3);
                    result.VbpPath = result.Clsid;
                    result.Version = new BuildVersion("0.0", false);
                    result.File = new ModuleFile(result.Clsid);
                    break;

                case (DepType.Tlb):
                    result.Clsid = temp[0].Substring(3);
                    result.Version = new BuildVersion(temp[1], false);
                    result.Lcid = temp[2];
                    result.File = new ModuleFile(temp[3]);
                    result.Desc = temp[4];
                    break;

                case (DepType.Reference):
                    /*story: vb likes to store version for references in hex but
                     but objects are version in decimal for no special reason
                     however when stored in registry windows uses always hex version
                     */
                    result.Clsid = temp[0].Substring(3);
                    result.Version = new BuildVersion(temp[1], true);
                    result.Lcid = temp[2];
                    result.File = new ModuleFile(temp[3]);
                    result.Desc = temp[4];
                    break;
                case (DepType.Object):
                    result.Clsid = temp[0];
                    result.Version = new BuildVersion(temp[1], false);
                    result.Lcid = temp[2].Split(';')[0].Trim();
                    result.File = new ModuleFile(temp[2].Split(';')[1].Trim());
                    result.Desc = temp[2];
                    break;
            }
            if (result.Desc != null && result.Desc.Length > 50)
                result.Desc = result.Desc.Substring(0, 30);

            return result;
        }
        private static void ExtractIlInfo(string file, out string typeLibId, out int versionMajor, out int versionMinor, out int lcId, out string desc)
        {
            var assembly = Assembly.LoadFrom(file);
            typeLibId = string.Format("{{{0}}}", Marshal.GetTypeLibGuidForAssembly(assembly));
            Marshal.GetTypeLibVersionForAssembly(assembly, out versionMajor, out versionMinor);
            lcId = 0;
            desc = assembly.FullName;
        }
        private static void ExtractComInfo(string file, out string typeLibId, out int versionMajor, out int versionMinor, out int lcId, out string desc)
        {
            ITypeLib typeLib = null;
            var pointer = IntPtr.Zero;

            try
            {

                API.LoadTypeLibEx(file, API.RegKind.RegKind_Register, out typeLib);
                typeLibId = string.Format("{{{0}}}",Marshal.GetTypeLibGuid(typeLib));

                typeLib.GetLibAttr(out pointer);
                var tmp = (TYPELIBATTR)Marshal.PtrToStructure(pointer, typeof(TYPELIBATTR));
                versionMajor = tmp.wMajorVerNum;
                versionMinor = tmp.wMinorVerNum;
                lcId = tmp.lcid;
                typeLib.ReleaseTLibAttr(pointer);
                pointer = IntPtr.Zero;
                desc = Marshal.GetTypeLibName(typeLib);
            }
            finally
            {
                if (typeLib != null)
                {
                    if (pointer != IntPtr.Zero)
                        typeLib.ReleaseTLibAttr(pointer);

                    Marshal.ReleaseComObject(typeLib);
                }
            }
        }

        private static void validateModuleIntegrity(string modulePath, string hash)
        {
            if (string.Compare(Utility.ComputeHash(modulePath), hash, true) != 0)
                throw new Exception("Module is not build in master build or changed outside build:" + modulePath);
        }

        public ModuleID(string file, string hash)
        {
            if(hash!=null)
                validateModuleIntegrity(file, hash);

            File = new ModuleFile(file);
            int major, minor, lcid;
            string tid, desc;

            switch (File.BinaryType)
            {
                case ModuleFile.ModuleType.Com:
                    ExtractComInfo(file, out tid, out major, out minor, out lcid, out desc);
                    break;
                case ModuleFile.ModuleType.Il:
                    ExtractIlInfo(file, out tid, out major, out minor, out lcid, out desc);
                    break;
                case ModuleFile.ModuleType.Native:
                     Clsid = Path.GetFileName(file);
                    Version = BuildVersion.FromFile(file);
                    Lcid = "0";
                    Desc = "Native dll " + Clsid ;
                    return;
                default:
                    throw new Exception(string.Format("File {0} of type {1} is not reflectable.", file, File.BinaryType));
            }
            Clsid = tid;
            Version = new BuildVersion(major, minor);
            Lcid = lcid.ToString();
            Desc = desc;
        }
        private ModuleID()
        {
            
        }
        public ModuleID(string clsid, string lcid, string modulePath, BuildVersion version, string desc)
        {
            Clsid = clsid;
            Lcid = lcid;
            File = new ModuleFile(modulePath);
            Version = new BuildVersion(version);
            Desc = desc;
        }

        public ModuleID(string clsid, string lcid, string modulePath, short versionMajor, short versionMinor, string desc)
            : this(clsid, lcid, modulePath, new BuildVersion(versionMajor, versionMinor), desc)
        {
            
        }

        #endregion
        #region DepType enum

        public enum DepType
        {
            Reference,
            Object,
            Tlb,
            VBP
        }

        #endregion

        public string Name
        {
            get { return Path.GetFileName(File.ValidPath).ToLower(); }
        }

        public static string GetRegPath(string cls, BuildVersion ver, string langId)
        {
            return string.Join(("\\"), cls, ver, langId, "win32");
        }
        public string RegistryPath
        {
            get { return GetRegPath(Clsid, Version, Lcid); }
        }

        private static DepType getTypeFromLine(string refLine)
        {
            if(refLine.ToLower().Contains(".tlb#"))
                return DepType.Tlb;

            if (refLine.StartsWith("Object"))
                return DepType.Object;

            if (refLine.ToLower().EndsWith("vbp"))
                return DepType.VBP;

            return DepType.Reference;
        }

        public override int GetHashCode()
        {
            return (Clsid + Version + Lcid + Type).GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj is ModuleID)
            {
                var lhs = obj as ModuleID;

                return ((string.Compare(Clsid, lhs.Clsid, true) == 0) &&
                        Version.Compare(lhs.Version) == 0 &&
                        (lhs.Type == Type) &&
                        (lhs.Lcid == Lcid));
            }
            return false;
        }

        public void UpdateIf(ModuleID other)
        {
            Upgraded = !Version.Equals(other.Version);
            if (!Equals(other) || !File.Equals(other.File) || other.Version.Compare(Version) !=0)
            {
                Clsid = other.Clsid;
                Version.Set(other.Version);
                Lcid = other.Lcid;
                File = other.File;
                return;
            }
            return;
        }

        public void AddVbpFullPath(string dir)
        {
            if (Type != DepType.VBP)
                throw new Exception();

            VbpFullPath = Path.Combine(dir, VbpPath);
        }

        public override string ToString()
        {
            if (Type == DepType.VBP)
                throw new InvalidOperationException("Can't Translate VBP references.");

            return Type == DepType.Object ? 
                string.Format("Object={0}#{1}#{2}; {3}", Clsid, Version.Decimal, Lcid, Name) : 
                string.Format(@"Reference=*\G{0}#{1}#{2}#{3}#{4}", Clsid, Version.Hex, Lcid, File.Raw, Desc);
        }

        public void Upgrade(string path)
        {
            Upgrade(Version, path);
        }

        public void Upgrade(BuildVersion v , string path)
        {
            if (v.Compare(Version) != 0)
            {
                Version.Set(v);
                Upgraded = true;
            }
            if(!string.IsNullOrEmpty(path))
                File.Reset(path);

            UpdateIf(new ModuleID(path, null));
        }

        public bool InterfaceCompatible(ModuleID id)
        {
            return id.Clsid == Clsid && id.Lcid == Lcid&& id.Type == Type;
        }

        public bool ModuleInCompatible(ModuleID id)
        {
            //todo: decides if you want to verify or not.
            return false;
            if (id.Name != Name)
                return false;
            if (id.Type != Type)
                return false;

            return Clsid != id.Clsid;
        }
        public static ModuleID GetBetterVersion(ModuleID l, ModuleID m)
        {
            if (!l.InterfaceCompatible(m))
                throw new InvalidOperationException(string.Format("Incompatible Coms: \n{0}\n{1}", l.RegistryPath,
                                                                  m.RegistryPath));
            return l.Version.Compare(m.Version) > 0 ? l : m;
        }

        public bool IsBetterVersion(ModuleID found)
        {
            if(found.Clsid != Clsid)
                return false;

            var cmp = Version.Compare(found.Version);
            if(cmp > 0)
                return true;
            if(cmp < 0)
                return false;

            return found.File.Date > File.Date;
        }
        #region Registry Verification
        private static SortedList<BuildVersion, string> ReadTypeLibVersionFromReg(string path)
        {
            var spl = path.Split('\\');
            var root = Registry.ClassesRoot.OpenSubKey("TypeLib").OpenSubKey(spl[0]);
            var lst = new SortedList<BuildVersion, string>(new BuildVersion.VersionComparer());
            if (root == null)
                return lst;

            var srch = root.GetSubKeyNames();

            foreach (var k in srch)
            {
                var p = root.OpenSubKey(k)
                    .OpenSubKey(spl[2])
                    .OpenSubKey("Win32")
                    .GetValue(String.Empty).ToString();
                
                if (ModuleFile.Exists(p))
                    lst.Add(new BuildVersion(k, true), p.ToLower());
            }
            return lst;
        }
        public string GetPathFromRegistry()
        {
            var dll = string.Empty;
            if(API.ReadTypeLibPathFromReg(RegistryPath, ref dll) != API.ComLost.Found)
            {
                var version = ReadTypeLibVersionFromReg(RegistryPath);
                if(version.Count == 0)
                    return string.Empty;

                var last = version.Count - 1;
                dll = version.Values[last];
                return dll;
            }


            if(dll.ToLower().EndsWith(".tlb"))
            {
                var il = Utility.ChangeExt(dll, "dll");
                if (System.IO.File.Exists(il))
                    dll = il;
            }
            return dll;
        }
        public void VerifyRegistration(bool tryFix)
        {
            var regFile = String.Empty;
            var comStat = API.ReadTypeLibPathFromReg(RegistryPath, ref regFile);

            switch (comStat)
            {
                case API.ComLost.Version:
                    var vers = ReadTypeLibVersionFromReg(RegistryPath);
                    Upgrade(vers.Keys[0], vers.Values[0]);
                    return;
                case API.ComLost.Found:
                    if(ModuleIsFoundInExtern())
                        Upgrade(GetModulePathFromExtern());
                    else
                        Upgrade(regFile);
                    return;
                    
                default:
                    if (tryFix)
                    {
                        TryFixingReference();
                        return;
                    }

                    throw new Exception(
                        String.Format(
                            "Module Registration validation failed for {0}\nExpected Path:{1}\nRegistration Path:{2}",
                            RegistryPath, File.Raw, regFile));

            }


        }

        private string GetModulePathFromExtern()
        {
            return Path.Combine(Environment.CurrentDirectory, "extern", Name);
        }
        private bool ModuleIsFoundInExtern()
        {
            return File.Dir.ToLower() == Path.GetDirectoryName(GetModulePathFromExtern());
        }
        private void TryFixingReference()
        {
            if (System.IO.File.Exists(GetModulePathFromExtern()))
            {
                Upgrade(GetModulePathFromExtern());
                File.VerifyExist();
            }
            else
                File.VerifyExist();

            Utility.Register(File.Raw);
            VerifyRegistration(false);
        }

        #endregion
    }
}